import url from '@ohos.url'
import HashMap from '@ohos.util.HashMap'
import {
  Body,
  CONNECT,
  DELETE,
  GET,
  HEAD,
  Header,
  HeaderMap,
  Headers,
  HTTP,
  HttpDecorator,
  HttpMappingValue,
  OPTIONS,
  Path,
  POST,
  PUT,
  Query,
  QueryMap,
  TRACE,
  Url
} from './HttpDecorators'
import { Meta, MethodMetaContext, ParameterMetaContext } from './Meta'
import { Request } from './Request'
import { Utils } from './Utils'
import { HttpMethod } from './HttpConstants'

export class RequestFactory {
  static PARAM_URL_REGEX = /{([a-zA-Z][a-zA-Z0-9_-]*)}/g
  body: any
  gotBody = false
  private httpMethod: HttpMethod
  private header: HashMap<string, string>
  private pathMapping: HashMap<string, string>
  private queryMapping: HashMap<string, string>
  private baseUrl: string
  private relativeUrl: string

  constructor(private builder: RequestFactory.Builder) {
    this.httpMethod = builder.httpMethod
    this.header = builder.header
    this.pathMapping = builder.pathMapping
    this.queryMapping = builder.queryMapping
    this.baseUrl = builder.baseUrl
    this.relativeUrl = builder.relativeUrl
    this.body = builder.body
    this.gotBody = builder.gotBody
  }

  static parseMetas(httpDecorator: HttpDecorator, methodMetaContext: MethodMetaContext): RequestFactory {
    return new RequestFactory.Builder(httpDecorator, methodMetaContext).build()
  }

  create(): Request {
    if (Utils.isUndefined(this.httpMethod) || !this.httpMethod) {
      this.httpMethod = HttpMethod.GET
    }
    const builder = new Request.Builder(this.builder.httpDecorator)
      .setBaseUrl(this.baseUrl)
      .setUrl(this.buildRelativeUrl())
      .addHeaders(this.header)
      .addQueryParams(this.queryMapping)
      .setHttpMethod(this.httpMethod)

    if (this.gotBody) {
      builder.setBody(this.body)
    }
    return builder.build()
  }


  private buildRelativeUrl(): string {
    let url = this.relativeUrl || ""
    if (url.startsWith('/')) {
      url = url.slice(1)
    }

    url = url.replace(RequestFactory.PARAM_URL_REGEX, (match, index) => {
      const pathValue = this.pathMapping.get(index)
      return typeof pathValue !== "undefined" ? pathValue : match;
    });

    return url
  }
}

export namespace RequestFactory {
  export class Builder {
    httpMethod: HttpMethod
    header: HashMap<string, string>
    pathMapping: HashMap<string, string>
    queryMapping: HashMap<string, string>
    forceUrl: string
    baseUrl: string
    relativeUrl: string
    body: any

    gotBody = false
    gotUrl = false
    gotPath = false
    gotQuery = false
    gotQueryMap = false

    private metaHttpMethodMap = new Map<Meta, HttpMethod>([
      [GET, HttpMethod.GET],
      [OPTIONS, HttpMethod.OPTIONS],
      [HEAD, HttpMethod.HEAD],
      [POST, HttpMethod.POST],
      [PUT, HttpMethod.PUT],
      [DELETE, HttpMethod.DELETE],
      [TRACE, HttpMethod.TRACE],
      [CONNECT, HttpMethod.CONNECT]
    ])

    constructor(public httpDecorator: HttpDecorator, public methodMetaContext: MethodMetaContext) {
      this.header = new HashMap<string, string>()
      this.baseUrl = null
      this.pathMapping = new HashMap<string, string>()
      this.queryMapping = new HashMap<string, string>()
    }

    build(): RequestFactory {
      this.methodMetaContext.getMetaScope().forEach((vs, k) => {
        if (vs) {
          vs.forEach((v, i) => {
            this.parseMethodMeta(k, v)
          })
        } else {
          this.parseMethodMeta(k, vs)
        }


      })
      this.methodMetaContext.parameterMetaContexts.forEach((v, k) => {
        v.getMetaScope()?.forEach((vs, k) => {
          if (vs) {
            vs.forEach((meta, i) => {
              this.parseParametersMeta(v, k, meta)
            })
          } else {
            this.parseParametersMeta(v, k, vs)
          }
        })
      })
      if (this.forceUrl) {
        this.baseUrl = this.forceUrl
      }
      return new RequestFactory(this)
    }

    /**
     * 解析元数据
     * @param key
     * @param value
     */
    private parseMethodMeta(meta: Meta, value: any) {
      switch (meta) {
        case OPTIONS:
        case GET:
        case HEAD:
        case POST:
        case PUT:
        case DELETE:
        case TRACE:
        case CONNECT:
          if (typeof value !== 'string') {
            value = ''
          }
          this.parseHttpMethodAndPath(this.metaHttpMethodMap.get(meta), value);
          break
        case HTTP:
          const mValue = value as HttpMappingValue
          this.parseHttpMethodAndPath(mValue.method, mValue.path);
          break
        case Headers:
          this.parseMethodHttpParameter(meta, value)
          break
      }
    }

    private parseMethodHttpParameter(meta: Meta, value: any) {
      switch (meta) {
        case Headers:
          const headers = value as Array<string>
          if (!headers || headers.length == 0) {
            throw Utils.methodError(
              this.methodMetaContext,
              "err",
              "@Headers decorator is empty.",
              null);
          }
          headers.forEach((header, i) => {
            const colon = header.indexOf(':')
            if (colon == -1 || colon == 0 || colon == header.length - 1) {
              throw Utils.methodError(
                this.methodMetaContext,
                "err",
                "@Headers value must be in the form \"Name: Value\". Found: \"{}\"",
                header);
            }
            const headerName = header.substring(0, colon);
            const headerValue = header.substring(colon + 1).trim();

            if ("content-type" === headerName.toLowerCase()) {
              //todo 校验content-type
            }
            this.header.set(headerName, headerValue)
          })
          break
      }
    }

    /**
     *
     * @param path
     * @returns
     */
    private parseParametersMeta(context: ParameterMetaContext, meta: Meta, value: any) {
      switch (meta) {
        case Body:
        case Path:
        case Header:
        case HeaderMap:
        case QueryMap:
        case Url:
        case Query: {
          this.parseHttpParameter(context, meta, value)
          break
        }
      }
    }

    private parseHttpParameter(context: ParameterMetaContext, decoratorName: Meta, name: string) {
      const value = context.value
      switch (decoratorName) {
        case QueryMap: {
          this.gotQueryMap = true
          Object.entries(value)?.forEach((e, i) => {
            this.queryMapping.set(e[0], e[1] + "")
          })
          break
        }
        case Query: {
          this.gotQuery = true
          this.queryMapping.set(name, value)
          break
        }
        case Url: {
          if (this.gotUrl) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "Multiple @Url method annotations found.")
          }
          if (this.gotPath) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "@Path parameters may not be used with @Url.");
          }
          if (this.gotQuery) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "A @Url parameter must not come after a @Query.");
          }
          // if (this.gotQueryName) {
          //   throw parameterError(method, p, "A @Url parameter must not come after a @QueryName.");
          // }
          if (this.gotQueryMap) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "A @Url parameter must not come after a @QueryMap.");
          }
          this.gotUrl = true;
          this.forceUrl = value
          break
        }
        case Path: {
          if (this.gotQuery) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "A @Path parameter must not come after a @Query.");
          }
          if (this.gotQueryMap) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "A @Path parameter must not come after a @QueryMap.");
          }
          if (this.gotUrl) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "@Path parameters may not be used with @Url.");
          }
          this.gotPath = true;
          this.pathMapping.set(name, value)
          break
        }
        case Body: {
          if (this.gotBody) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "Multiple @Body method annotations found.")
          }
          if (typeof value === undefined) {
            throw Utils.parameterError(this.methodMetaContext, context.index, "Unable to create @Body for 'undefined'")
          }
          this.body = value
          this.gotBody = true
          break
        }
        case Header: {
          this.header.set(name, value)
          break
        }
        case HeaderMap: {
          Object.entries(value)?.forEach((e, i) => {
            this.header.set(e[0], e[1] + "")
          })
          break
        }
      }
    }

    private parseHttpMethodAndPath(httpMethod: HttpMethod, value: string) {
      if (this.httpMethod != null) {
        throw Utils.methodError(
          this.methodMetaContext,
          "err",
          "Only one HTTP method is allowed. Found: {} and {}.",
          this.httpMethod.method,
          httpMethod.method);
      }

      if (Utils.isEmpty(value)) {
        return
      }

      this.httpMethod = httpMethod
      //解析query参数
      const question = value.indexOf('?')
      if (question != -1 && question < value.length - 1) {
        //取出参数部分
        const queryParams = value.substring(question + 1)
        if (queryParams.match(RequestFactory.PARAM_URL_REGEX).length != 0) {
          throw Utils.methodError(
            this.methodMetaContext,
            "err",
            "URL query string \"{}\" must not have replace block. "
              + "For dynamic query parameters use @Query.",
            this.httpMethod.method,
            httpMethod.method);
        }
      }
      let inputUrl: url.URL
      try {
        //如果是完整链接
        if (Utils.isHttpOrHttps(value)) {
          inputUrl = url.URL.parseURL(value)
          this.baseUrl = inputUrl.origin
          this.relativeUrl = decodeURI(inputUrl.pathname)
        }
        //如果是相对路径
        else {
          this.relativeUrl = value
          inputUrl = url.URL.parseURL(value, this.baseUrl)
        }
      } catch (e) {
      }


      inputUrl?.params.forEach((v, k) => {
        this.queryMapping.set(k, v)
      })

    }
  }
}


